#lang racket

;; Você deve implementar as funções neste arquivo. Novas funções podem ser
;; criadas, mas todas as funções devem ter testes (no arquivo testes.rkt).
;;
;; Observe que algumas destas funções não tem testes, faz parte do trabalho
;; criar estes testes.
;;
;; Você não precisa se preocupar com ler a tecla pressionada ou desenhar o jogo
;; na tela. O arquivo main.rkt chama uma função que faz isso. Basta você
;; implementar as funções deste arquivo que o jogo funciona.
;;
;; Para ter uma ideia do processo de execução do jogo, execute o arquivo
;; main.rkt sem mudar nada neste arquivo. Uma janela irá aparecer. Pressione
;; algumas teclas e observe a saída no console do DrRacket. Veja o corpo
;; inicial das funções make-tetris-padrao, trata-tecla, trata-tick e desenha.

(require "base.rkt")
(require 2htdp/image)
(require 2htdp/universe)

(provide make-tetris-padrao
         tetramino->lista-pos
         lop-validas?
         lop-livres?
         fixa
         limpa
         rotaciona
         trata-tecla-esquerda
         trata-tecla-direita
         trata-tecla-cima
         trata-tecla-baixo
         trata-tecla
         trata-deslocamento
         desloca-tetra-para-baixo
         trata-tick
         desenha
         stream-tetraminos
         fixa-tetramino-no-campo
         reseta-timeout
         chegou-ao-fim)

;; -> Tetris
;; Cria o jogo inicial.
;; Esta função é chamada no arquivo main.rkt.
(define (make-tetris-padrao)
  (make-tetris LARGURA-PADRAO ALTURA-PADRAO (stream-tetraminos) TIMEOUT-PADRAO))

;; Tetris KeyEvent -> Tetris
;; Esta função é chamada quando uma tecla é pressionada.
;; Devolve um jogo com o tetraminó que está caindo movido de acordo com a tecla
;;   "right" - tenta mover para direita
;;   "left"  - tenta mover para esquerda
;;   "up"    - tenta rotacionar
;;   "down"  - tenta mover para baixo
;;
;; Se a tecla for "right", "left" ou "up" e o movimento não puder ser
;; realizado, o jogo é devolvido sem modificações.
;;
;; Se a tecla for "down" e o movimento não puder ser realizado, tetra é fixado
;; no campo, as linhas completas são removidas, o próximo tetraminó é
;; selecionada para cair e o contador de automovimento retorna ao valor
;; inicial.
;;
;; Se o movimento puder ser realizado, o jogo após o movimento é devolvido.
;;
;; Use a função key=? para comparar o tecla com os valores "right", "left, "up"
;; e "down".
(define (trata-tecla jogo tecla)
  (cond 
    [(key=? tecla "left")  (trata-tecla-esquerda jogo)]
    [(key=? tecla "right") (trata-tecla-direita jogo)]
    [(key=? tecla "up")    (trata-tecla-cima jogo)]
    [(key=? tecla "down")  (trata-tecla-baixo jogo)]
    [else jogo]))

;; Tetris -> Tetris
;; Função que trata o movimento do tetraminó para a esquerda. Se for possível,
;; retornará o jogo com o tetraminó em uma posição à esquerda (decrementando a
;; coluna em 1). Caso contrário, o jogo é devolvido sem modificações.
(define (trata-tecla-esquerda jogo)
  (trata-deslocamento jogo (lambda (pos) (posn (posn-lin pos) (sub1 (posn-col pos))))))

;; Tetris -> Tetris
;; Função que trata o movimento do tetraminó para a direita. Se for possível,
;; retornará o jogo com o tetraminó em uma posição à direita (incrementando a
;; coluna em 1). Caso contrário, o jogo é devolvido sem modificações.
(define (trata-tecla-direita jogo)
  (trata-deslocamento jogo (lambda (pos) (posn (posn-lin pos) (add1 (posn-col pos))))))

;; Tetris -> Tetris
;; Função que trata a rotação do tetraminó. Se for possível, retornará o jogo
;; com o tetraminó rotacionado. Caso contrário, o jogo é devolvido sem modificações.
(define (trata-tecla-cima jogo)
  (trata-movimento jogo (lambda (tetra) (rotaciona tetra))))

;; Tetris -> Tetris
;; Função que trata o movimento do tetraminó para baixo. Se for possível, retornará
;; o jogo com o tetraminó em uma posição para baixo (incrementando a linha em 1).
;; Caso contrário, o jogo é devolvido sem modificações.
(define (trata-tecla-baixo jogo)
  (desloca-tetra-para-baixo (reseta-timeout jogo)))

;; Tetramino -> Tetramino
;; Retorna um tetraminó rotacionado. A nova rotação é calculada com base no resto
;; da divisão entre a rotação atual + 1 e a quantidade de rotações possíveis.
;;
;; Exemplo: um tetraminó que está na rotação 2 e possui 4 possíveis rotações irá
;; para a rotação 3, pois (remainder (+ 2 1) 4) é 3. Já um tetraminó que está na
;; rotação 3 e possui 4 possíveis rotações irá para a rotação 0, pois
;; (remainder (+ 3 1) 4) é 0.
(define (rotaciona tetra) 
  (struct-copy tetramino tetra
               [rot (remainder (+ (tetramino-rot tetra) 1) (length(tetramino-tipo tetra)))]))

;; Tetris -> Tetris
;; Função que move o tetraminó para baixo, adicionando 1 em sua posição na linha.
(define (desloca-tetra-para-baixo jogo)  
  (trata-deslocamento jogo (lambda (pos) (posn (add1 (posn-lin pos)) (posn-col pos)))))

;; Tetris -> Tetris
;; Retorna um jogo igual ao recebido, porém com o timeout zerado.
(define (reseta-timeout jogo) 
  (struct-copy tetris jogo
               [timeout 0]))

;; Tetris (Posn -> Posn) -> Tetris
;; Retorna um jogo igual ao recebido, porém com o tetramino deslocado de acordo
;; com a função f.
(define (trata-deslocamento jogo f)
  (trata-movimento jogo (lambda (tetra) (struct-copy tetramino tetra [pos (f (tetramino-pos tetra))]))))

;; Tetris (Tetramino -> Tetramino) -> Tetris
;; Remove o tetraminó que estava caindo e aplica a função f nele. Tenta inserí-
;; lo no campo. Se sua nova posição for válida e livre, retorna o jogo com o
;; tetraminó inserido.
(define (trata-movimento jogo f)
  (define (limpa-tetramino jogo)
    (struct-copy tetris jogo
                 [campo (fixa-tetramino-no-campo (tetramino->lista-pos(tetris-tetra jogo)) (tetris-campo jogo) 0 0 0)]
                 [tetra (struct-copy tetramino (tetris-tetra jogo)
                                     [cor 0])]))
  (define (valido? lista jogo) 
    (and (lop-validas? lista (tetris-largura jogo) (tetris-altura jogo))
         (lop-livres? lista (tetris-campo jogo))))      
  (define (tetra-movido tetra)
    (f tetra))  
  (define (move-tetramino tetra jogo-limpo)
    (struct-copy tetris jogo-limpo
                 [campo (fixa-tetramino-no-campo (tetramino->lista-pos (tetra-movido tetra)) (tetris-campo jogo-limpo) 0 0 (tetramino-cor tetra))]
                 [tetra (tetra-movido tetra)])) 
  (define (escolhe-melhor-jogo jogo jogo-limpo)
    (if (valido? (tetramino->lista-pos (tetra-movido (tetris-tetra jogo-limpo))) jogo-limpo)
        (move-tetramino (tetris-tetra jogo) jogo-limpo)
        jogo))  
  (escolhe-melhor-jogo jogo (limpa-tetramino jogo)))

;; Tetris -> Tetris
;; Função que trata um tick. Esta função é chamada 28 vezes por segundo, ela
;; deve mover o tetra para baixo depois de uma determinada quantidade
;; (TIMEOUT-PADRAO) de ticks. Se o jogador mover para baixo e fixar o
;; tetraminó, a contagem deve reiniciar.
(define (trata-tick jogo)
  (define (cria-jogo-novo-tetra jogo)
    (struct-copy tetris jogo
                 [tetra (centraliza (stream-first (tetris-proximos jogo)) (tetris-largura jogo)) ]
                 [proximos (stream-rest (tetris-proximos jogo))]))
  (define (jogo-esta-fixado? jogo-anterior jogo-atual)
    (equal? (tetris-tetra jogo-anterior) (tetris-tetra jogo-atual)))
  (define (incrementa-timeout jogo) 
    (struct-copy tetris jogo
                 [timeout (add1(tetris-timeout jogo))]))  
  (define(trata-timeout jogo-anterior jogo-atual)
    (if (jogo-esta-fixado? jogo-anterior jogo-atual)
        (cria-jogo-novo-tetra (limpa jogo-atual))
        (incrementa-timeout jogo-atual)))
  (if (= (tetris-timeout jogo) TIMEOUT-PADRAO)
      (trata-timeout jogo (desloca-tetra-para-baixo (reseta-timeout jogo)))
      (incrementa-timeout jogo)))

;; Tetris -> Imagem
;; Esta função é chamada quando o jogo precisa ser desenhado na tela. Devolve
;; uma imagem que representa o jogo.
;; Veja as funções pré-definidas rectangle, beside, above e overlay (entre
;; outras)no pacote 2htdp/image.
(define (desenha jogo)
  (define (cor->rectangle number)
    (rectangle Q-LARGURA Q-ALTURA "solid" (list-ref CORES number)))
  (define (campo->image campo)
    (cond
      [(empty? campo) (rectangle 0 0 "solid" "black")]
      [(list? (first campo))
       (above (campo->image (first campo))
              (campo->image (rest campo)))]
      [else (beside (cor->rectangle (first campo))
                    (campo->image (rest campo)))]))
  (campo->image (tetris-campo jogo)))

;; Tetramino -> Lista(Posn)
;; Devolve a lista de posições que t ocupa no campo considerando a rotação e a
;; posição (translação em relação a origem).
;; 
;; Por exemplo, seja TT1 definido como
;; (define TT1 (tetramino T_TIPOS 1 (posn 1 0) T_COR))
;; este tetraminó está na rotação 1 e na posição (posn 1 0). O elemento na
;; posição 1 de T_TIPOS é T1 que é a seguinte lista de listas (definina em
;; tetra-tipos.rkt)
;;    0 1 2     ; colunas
;;              ; linhas
;; '((0 1 0)    ; 0
;;   (0 1 1)    ; 1
;;   (0 1 0)))  ; 2
;;
;; As as posições ocupadas por T1 são marcadas com 1, ou seja, as posições
;; ocupadas por T1 são (posn 0 1) (posn 1 1) (posn 1 2) e (posn 2 1). Estas São
;; as posições em relação a (posn 0 0), mas o tetraminó está na posição
;; (posn 1 0), desta forma, precisamos fazer a translação das posições. Para
;; isto, somamos o ponto (posn 1 0) a cada ponto de T1, o que resulta em
;; (pos 1 1) (posn 2 1) (posn 2 2) (posn 3 1). Observe que é posível ter
;; um deslocamento em relação a origem negativa. Por exemplo, se a posição de
;; TT1 fosse (posn 0 -1), obteríamos como resposta da função a lista com
;; as posições (posn 0 0) (posn 1 0) (pos 1 1) (pos 2 0).
;;
;; Veja os testes para outros exemplos de como esta função deve funcionar.
(define (tetramino->lista-pos t)  
  (define (cria-posn parteTetramino linha coluna posicaoNoCampo)
    (cond
      [(empty? parteTetramino) empty]
      [else (cons (if (< 0 (first parteTetramino)) (posn (+ (posn-lin posicaoNoCampo) linha) (+ (posn-col posicaoNoCampo) coluna)) empty)
                  (cria-posn (rest parteTetramino) linha (add1 coluna) posicaoNoCampo))]))
  (define (cria-lista-posn tetramino linha posicaoNoCampo)
    (cond
      [(empty? tetramino) empty]
      [else (append (cria-posn (first tetramino) linha 0 posicaoNoCampo)
                    (cria-lista-posn (rest tetramino) (add1 linha) posicaoNoCampo))]))
  (flatten (cria-lista-posn (list-ref (tetramino-tipo t) (tetramino-rot t)) 0 (tetramino-pos t))))

;; Lista(Posn) Natural Natural -> Boolean
;; Devolve verdadeiro se todas os posições de lp são válidas, isto é, estão
;; dentro de um campo de tamanho largura x altura. Devolve falso caso
;; contrário.
(define (lop-validas? lp largura altura)
  (define (posn-dentro-campo? p)
    (and (and (< (posn-col p) largura)
              (>= (posn-col p) 0))
         (and (< (posn-lin p) altura)
              (>= (posn-lin p) 0))))
  (cond
    [(empty? lp) #t]
    [else (and (posn-dentro-campo? (first lp))
               (lop-validas? (rest lp) largura altura))]))

;; Lista(Posn) Campo -> Boolean
;; Devolve verdadeiro se todas as posição de lp estão livres no campo. Devolve
;; falso caso contrário.
;; Requer que todas as posições em lp sejam válidas.
;; LEMBRAR: chamar lop-validas antes.
(define (lop-livres? lp campo)
  (define (posn-valido? p) 
    (= 0 (list-ref (list-ref campo (posn-lin p)) (posn-col p))))
  (cond
    [(empty? lp) #t]
    [else (and (posn-valido? (first lp))
               (lop-livres? (rest lp) campo))]))

;; Lista(Posn) Campo Natural Natural Cor -> Campo
;; Percorre o campo, verificando se a posição (linha, coluna) é a mesma posição
;; especificada em algum dos posn. Em caso afirmativo, "insere" nesta posição a
;; cor do tetraminó. Retorna um novo campo, com as posições especificadas nos
;; posn do tetraminó pintadas.
(define (fixa-tetramino-no-campo listaPos campo linha coluna cor)
  (define (posicao-campo-equivale-posn linha coluna listaPos)
    (cond
      [(empty? listaPos) #f]
      [else (or (and (= linha (posn-lin (first listaPos)))
                     (= coluna (posn-col (first listaPos))))
                (posicao-campo-equivale-posn linha coluna (rest listaPos)))]))
  (cond
    [(empty? campo) empty]
    [(list? (first campo))
     (cons (fixa-tetramino-no-campo listaPos (first campo) linha coluna cor)
           (fixa-tetramino-no-campo listaPos (rest campo) (add1 linha) coluna cor))]
    [else (cons (if (posicao-campo-equivale-posn linha coluna listaPos)
                    cor
                    (first campo))
                (fixa-tetramino-no-campo listaPos (rest campo) linha (add1 coluna) cor))]))

;; Tetris -> Tetris
;; Preenche as posições ocupadas pelo tetraminó (que está caindo) no campo do
;; jogo.
;; Requer que tetraminó não possa ser movido para baixo.
(define (fixa jogo)
  
  (define (incrementa-linha-posn posnP)
    (posn (add1 (posn-lin posnP)) (posn-col posnP)))
  (define (incrementa-linha-lista-pos listaPos)
    (cond
      [(empty? listaPos) empty]
      [else (cons (incrementa-linha-posn (first listaPos))
                  (incrementa-linha-lista-pos (rest listaPos)))]))
  (if (lop-livres? (flatten (incrementa-linha-lista-pos (tetramino->lista-pos (tetris-tetra jogo)))) (tetris-campo jogo))
      jogo
      (struct-copy tetris jogo
                   [campo (fixa-tetramino-no-campo (tetramino->lista-pos (tetris-tetra jogo)) (tetris-campo jogo) 0 0 (tetramino-cor (tetris-tetra jogo)))])))

;; Tetris -> Tetris
;; Devolve um jogo sem as linhas que estão completas, isto é, as linhas que não
;; tem nenhum quadrado vazio. O jogo devolvido tem o mesmo tamanho do jogo de
;; entrada.
(define (limpa jogo)
  (define (preenche-jogo campo)
    (cond
      [(= (tetris-altura jogo) (length campo)) campo]
      [else (preenche-jogo (cons (make-linha (tetris-largura jogo)) campo))]))
  (define (linha-nao-completa lst)
    (cond
      [(empty? lst) #f]
      [else (or (= 0 (first lst))
                (linha-nao-completa (rest lst)))]))
  (struct-copy tetris jogo
               [campo (preenche-jogo (filter linha-nao-completa (tetris-campo jogo)))]))

;; -> Stream(Tetramino)
;; Cria um stream randômico de tetraminós.
;; Esta função não precisa de testes.
;; Você tem que implementar esta função, o corpo incial deve ser descartado.
(define (stream-tetraminos)      
  (stream-cons (list-ref (list I J L O S T Z) (random (length '(I J L O S T Z)))) (stream-tetraminos)))

(define (chegou-ao-fim jogo)
  (define (primeira-linha-livre linha)
  (cond
    [(empty? linha) #t]
    [else (and (= 0 (first linha))
               (primeira-linha-livre (rest linha)))]))
  ;(not (primeira-linha-livre (list-ref (tetris-campo jogo) 0)))
  #f)